package service

import (
	"context"

	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"github.com/pkg/errors"
	"github.com/stackrox/rox/central/convert/storagetov2"
	"github.com/stackrox/rox/central/convert/v2tostorage"
	"github.com/stackrox/rox/central/vulnmgmt/vulnerabilityrequest/common"
	"github.com/stackrox/rox/central/vulnmgmt/vulnerabilityrequest/datastore"
	"github.com/stackrox/rox/central/vulnmgmt/vulnerabilityrequest/manager/requestmgr"
	v2 "github.com/stackrox/rox/generated/api/v2"
	"github.com/stackrox/rox/pkg/auth/permissions"
	"github.com/stackrox/rox/pkg/errox"
	"github.com/stackrox/rox/pkg/grpc/authz"
	"github.com/stackrox/rox/pkg/grpc/authz/or"
	"github.com/stackrox/rox/pkg/grpc/authz/perrpc"
	"github.com/stackrox/rox/pkg/grpc/authz/user"
	"github.com/stackrox/rox/pkg/sac/resources"
	"github.com/stackrox/rox/pkg/search"
	"github.com/stackrox/rox/pkg/search/paginated"
	"google.golang.org/grpc"
)

const (
	defaultPageSize = 100
)

var (
	authorizer = perrpc.FromMap(map[authz.Authorizer][]string{
		or.Or(
			user.With(permissions.View(resources.VulnerabilityManagementRequests)),
			user.With(permissions.View(resources.VulnerabilityManagementApprovals))): {
			v2.VulnerabilityExceptionService_GetVulnerabilityException_FullMethodName,
			v2.VulnerabilityExceptionService_ListVulnerabilityExceptions_FullMethodName,
		},
		or.Or(
			user.With(permissions.Modify(resources.VulnerabilityManagementRequests)),
			user.With(permissions.Modify(resources.VulnerabilityManagementApprovals))): {
			v2.VulnerabilityExceptionService_CancelVulnerabilityException_FullMethodName,
			v2.VulnerabilityExceptionService_UpdateVulnerabilityException_FullMethodName,
		},
		user.With(permissions.Modify(resources.VulnerabilityManagementRequests)): {
			v2.VulnerabilityExceptionService_CreateDeferVulnerabilityException_FullMethodName,
			v2.VulnerabilityExceptionService_CreateFalsePositiveVulnerabilityException_FullMethodName,
			v2.VulnerabilityExceptionService_DeleteVulnerabilityException_FullMethodName,
		},
		user.With(permissions.Modify(resources.VulnerabilityManagementApprovals)): {
			v2.VulnerabilityExceptionService_ApproveVulnerabilityException_FullMethodName,
			v2.VulnerabilityExceptionService_DenyVulnerabilityException_FullMethodName,
		},
	})
)

// serviceImpl provides APIs for vulnerability exceptions.
type serviceImpl struct {
	v2.UnimplementedVulnerabilityExceptionServiceServer

	datastore datastore.DataStore
	manager   requestmgr.Manager
}

// RegisterServiceServer registers this service with the given gRPC Server.
func (s *serviceImpl) RegisterServiceServer(grpcServer *grpc.Server) {
	v2.RegisterVulnerabilityExceptionServiceServer(grpcServer, s)
}

// RegisterServiceHandler registers this service with the given gRPC Gateway endpoint.
func (s *serviceImpl) RegisterServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
	return v2.RegisterVulnerabilityExceptionServiceHandler(ctx, mux, conn)
}

// AuthFuncOverride specifies the auth criteria for this API.
func (s *serviceImpl) AuthFuncOverride(ctx context.Context, fullMethodName string) (context.Context, error) {
	return ctx, authorizer.Authorized(ctx, fullMethodName)
}

// GetVulnerabilityException returns the vulnerability exception with specified ID.
func (s *serviceImpl) GetVulnerabilityException(ctx context.Context, req *v2.ResourceByID) (*v2.GetVulnerabilityExceptionResponse, error) {
	obj, found, err := s.datastore.Get(ctx, req.GetId())
	if err != nil {
		return nil, err
	}
	if !found {
		return nil, errox.NotFound
	}
	return &v2.GetVulnerabilityExceptionResponse{
		Exception: storagetov2.VulnerabilityException(obj),
	}, nil
}

// ListVulnerabilityExceptions returns a list of vulnerability exceptions.
func (s *serviceImpl) ListVulnerabilityExceptions(ctx context.Context, req *v2.RawQuery) (*v2.ListVulnerabilityExceptionsResponse, error) {
	parsedQuery, err := search.ParseQuery(req.GetQuery(), search.MatchAllIfEmpty())
	if err != nil {
		return nil, errors.Wrap(errox.InvalidArgs, err.Error())
	}
	// Fill in pagination.
	paginated.FillPaginationV2(parsedQuery, req.GetPagination(), defaultPageSize)

	ret, err := s.datastore.SearchRawRequests(ctx, parsedQuery)
	if err != nil {
		return nil, err
	}

	return &v2.ListVulnerabilityExceptionsResponse{
		Exceptions: storagetov2.VulnerabilityExceptions(ret...),
	}, nil
}

// CreateDeferVulnerabilityException creates an exception request to deferral specified vulnerabilities.
func (s *serviceImpl) CreateDeferVulnerabilityException(ctx context.Context, req *v2.CreateDeferVulnerabilityExceptionRequest) (*v2.CreateDeferVulnerabilityExceptionResponse, error) {
	obj := v2tostorage.DeferVulnerabilityRequest(ctx, req)
	if err := s.manager.Create(ctx, obj); err != nil {
		return nil, errors.Wrap(err, "could not create deferral request")
	}
	return &v2.CreateDeferVulnerabilityExceptionResponse{
		Exception: storagetov2.VulnerabilityException(obj),
	}, nil
}

// CreateFalsePositiveVulnerabilityException creates an exception request to mark specified vulnerabilities as false positive.
func (s *serviceImpl) CreateFalsePositiveVulnerabilityException(ctx context.Context, req *v2.CreateFalsePositiveVulnerabilityExceptionRequest) (*v2.CreateFalsePositiveVulnerabilityExceptionResponse, error) {
	obj := v2tostorage.FalsePositiveVulnerabilityRequest(ctx, req)
	if err := s.manager.Create(ctx, obj); err != nil {
		return nil, errors.Wrap(err, "could not create deferral request")
	}
	return &v2.CreateFalsePositiveVulnerabilityExceptionResponse{
		Exception: storagetov2.VulnerabilityException(obj),
	}, nil
}

// ApproveVulnerabilityException approves a vulnerability exception. Once approved, the exception is enforced.
// The associated vulnerabilities are excluded from policy evaluation and risk evaluation, and the vulnerabilities
// may not appear in certain APIs responses by default.
func (s *serviceImpl) ApproveVulnerabilityException(ctx context.Context, req *v2.ApproveVulnerabilityExceptionRequest) (*v2.ApproveVulnerabilityExceptionResponse, error) {
	obj, err := s.manager.Approve(ctx, req.GetId(), &common.VulnRequestParams{Comment: req.GetComment()})
	if err != nil {
		return nil, err
	}
	return &v2.ApproveVulnerabilityExceptionResponse{
		Exception: storagetov2.VulnerabilityException(obj),
	}, nil
}

// DenyVulnerabilityException denies a vulnerability exception.
func (s *serviceImpl) DenyVulnerabilityException(ctx context.Context, req *v2.DenyVulnerabilityExceptionRequest) (*v2.DenyVulnerabilityExceptionResponse, error) {
	obj, err := s.manager.Deny(ctx, req.GetId(), &common.VulnRequestParams{Comment: req.GetComment()})
	if err != nil {
		return nil, err
	}
	return &v2.DenyVulnerabilityExceptionResponse{
		Exception: storagetov2.VulnerabilityException(obj),
	}, nil
}

// UpdateVulnerabilityException updates an existing vulnerability exception. The update is enforced only once it is approved.
// Currently, only the following can be updated:
// - CVEs and expiration time of the deferral exceptions
// - CVEs of the false positive requests
func (s *serviceImpl) UpdateVulnerabilityException(ctx context.Context, req *v2.UpdateVulnerabilityExceptionRequest) (*v2.UpdateVulnerabilityExceptionResponse, error) {
	if req.GetId() == "" {
		return nil, errox.InvalidArgs.CausedBy("vulnerability exception ID not specified")
	}
	if req.GetDeferralUpdate() == nil && req.GetFalsePositiveUpdate() == nil {
		return nil, errox.InvalidArgs.CausedBy("vulnerability exception update not specified")
	}

	update := &common.UpdateRequest{
		Comment: req.GetComment(),
	}
	if deferral := req.GetDeferralUpdate(); deferral != nil {
		update.DeferralUpdate = v2tostorage.DeferralUpdate(deferral)
	} else if fp := req.GetFalsePositiveUpdate(); fp != nil {
		update.FalsePositiveUpdate = v2tostorage.FalsePositiveUpdate(fp)
	}

	obj, err := s.manager.UpdateException(ctx, req.GetId(), update)
	if err != nil {
		return nil, errors.Wrapf(err, "updating vulnerability exception %s", req.GetId())
	}

	return &v2.UpdateVulnerabilityExceptionResponse{
		Exception: storagetov2.VulnerabilityException(obj),
	}, nil
}

// CancelVulnerabilityException cancels a vulnerability exception. Once cancelled, the exception is no longer
// enforced.
func (s *serviceImpl) CancelVulnerabilityException(ctx context.Context, req *v2.ResourceByID) (*v2.CancelVulnerabilityExceptionResponse, error) {
	obj, err := s.manager.Cancel(ctx, req.GetId())
	if err != nil {
		return nil, errors.Wrapf(err, "canceling vulnerability exception %s", req.GetId())
	}

	return &v2.CancelVulnerabilityExceptionResponse{
		Exception: storagetov2.VulnerabilityException(obj),
	}, nil
}

// DeleteVulnerabilityException deletes a vulnerability exception.
func (s *serviceImpl) DeleteVulnerabilityException(ctx context.Context, req *v2.ResourceByID) (*v2.Empty, error) {
	return &v2.Empty{}, s.manager.Delete(ctx, req.GetId())
}
